// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// This test is run using `flutter drive` by the CI (see /script/tool/README.md
// in this repository for details on driving that tooling manually), but can
// also be run using `flutter test` directly during development.

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:leak_tracker/leak_tracker.dart';
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
import 'package:webview_flutter_wkwebview/src/common/weak_reference_utils.dart';
import 'package:webview_flutter_wkwebview/src/common/web_kit.g.dart';
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';

Future<void> main() async {
  IntegrationTestWidgetsFlutterBinding.ensureInitialized();

  final HttpServer server = await HttpServer.bind(
    InternetAddress.loopbackIPv4,
    0,
  );
  unawaited(
    server.forEach((HttpRequest request) {
      if (request.uri.path == '/hello.txt') {
        request.response.writeln('Hello, world.');
      } else if (request.uri.path == '/secondary.txt') {
        request.response.writeln('How are you today?');
      } else if (request.uri.path == '/headers') {
        request.response.writeln('${request.headers}');
      } else if (request.uri.path == '/favicon.ico') {
        request.response.statusCode = HttpStatus.notFound;
      } else if (request.uri.path == '/http-basic-authentication') {
        final isAuthenticating = request.headers['Authorization'] != null;
        if (isAuthenticating) {
          request.response.writeln('Authorized');
        } else {
          request.response.headers.add(
            'WWW-Authenticate',
            'Basic realm="Test realm"',
          );
          request.response.statusCode = HttpStatus.unauthorized;
        }
      } else {
        fail('unexpected request: ${request.method} ${request.uri}');
      }
      request.response.close();
    }),
  );
  final prefixUrl = 'http://${server.address.address}:${server.port}';
  final primaryUrl = '$prefixUrl/hello.txt';
  final secondaryUrl = '$prefixUrl/secondary.txt';
  final headersUrl = '$prefixUrl/headers';
  final basicAuthUrl = '$prefixUrl/http-basic-authentication';

  setUp(() {
    PigeonOverrides.pigeon_reset();
  });

  testWidgets(
    'withWeakReferenceTo allows encapsulating class to be garbage collected',
    (WidgetTester tester) async {
      final gcCompleter = Completer<int>();
      final instanceManager = PigeonInstanceManager(
        onWeakReferenceRemoved: gcCompleter.complete,
      );

      ClassWithCallbackClass? instance = ClassWithCallbackClass();
      instanceManager.addDartCreatedInstance(instance.callbackClass);
      instance = null;

      // Force garbage collection.
      await forceGC();

      final int gcIdentifier = await gcCompleter.future;
      expect(gcIdentifier, 0);
    },
    // TODO(bparrishMines): See https://github.com/flutter/flutter/issues/148345
    skip: true,
    timeout: const Timeout(Duration(seconds: 10)),
  );

  testWidgets(
    'WKWebView is released by garbage collection',
    (WidgetTester tester) async {
      final webViewGCCompleter = Completer<void>();

      const webViewToken = -1;
      final finalizer = Finalizer<int>((int token) {
        if (token == webViewToken) {
          webViewGCCompleter.complete();
        }
      });

      PigeonOverrides.uIViewWKWebView_new =
          ({
            required WKWebViewConfiguration initialConfiguration,
            void Function(
              NSObject pigeonInstance,
              String? keyPath,
              NSObject? object,
              Map<KeyValueChangeKey, Object?>? change,
            )?
            observeValue,
          }) {
            final webView = UIViewWKWebView.pigeon_new(
              initialConfiguration: initialConfiguration,
            );
            finalizer.attach(webView, webViewToken);
            return webView;
          };
      PigeonOverrides.nSViewWKWebView_new =
          ({
            required WKWebViewConfiguration initialConfiguration,
            void Function(
              NSObject pigeonInstance,
              String? keyPath,
              NSObject? object,
              Map<KeyValueChangeKey, Object?>? change,
            )?
            observeValue,
          }) {
            final webView = NSViewWKWebView.pigeon_new(
              initialConfiguration: initialConfiguration,
            );
            finalizer.attach(webView, webViewToken);
            return webView;
          };
      // Wait for any WebView to be garbage collected.
      await tester.pumpWidget(
        Builder(
          builder: (BuildContext context) {
            return PlatformWebViewWidget(
              WebKitWebViewWidgetCreationParams(
                controller: PlatformWebViewController(
                  WebKitWebViewControllerCreationParams(),
                ),
              ),
            ).build(context);
          },
        ),
      );
      await tester.pumpAndSettle();
      await tester.pumpWidget(Container());

      // Force garbage collection.
      await IntegrationTestWidgetsFlutterBinding.instance.watchPerformance(
        () async {
          await tester.pumpAndSettle();
        },
      );

      await expectLater(webViewGCCompleter.future, completes);
    },
    timeout: const Timeout(Duration(seconds: 30)),
  );

  testWidgets('loadRequest', (WidgetTester tester) async {
    final pageFinished = Completer<void>();

    final controller = PlatformWebViewController(
      const PlatformWebViewControllerCreationParams(),
    );
    final delegate = PlatformNavigationDelegate(
      const PlatformNavigationDelegateCreationParams(),
    );
    unawaited(delegate.setOnPageFinished((_) => pageFinished.complete()));
    unawaited(controller.setPlatformNavigationDelegate(delegate));
    await controller.loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl)));

    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          return PlatformWebViewWidget(
            PlatformWebViewWidgetCreationParams(controller: controller),
          ).build(context);
        },
      ),
    );

    await pageFinished.future;

    final String? currentUrl = await controller.currentUrl();
    expect(currentUrl, primaryUrl);
  });

  testWidgets('runJavaScriptReturningResult', (WidgetTester tester) async {
    final pageFinished = Completer<void>();

    final controller = PlatformWebViewController(
      const PlatformWebViewControllerCreationParams(),
    );
    unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
    final delegate = PlatformNavigationDelegate(
      const PlatformNavigationDelegateCreationParams(),
    );
    unawaited(delegate.setOnPageFinished((_) => pageFinished.complete()));
    unawaited(controller.setPlatformNavigationDelegate(delegate));
    await controller.loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl)));

    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          return PlatformWebViewWidget(
            PlatformWebViewWidgetCreationParams(controller: controller),
          ).build(context);
        },
      ),
    );

    await pageFinished.future;

    await expectLater(
      controller.runJavaScriptReturningResult('1 + 1'),
      completion(2),
    );
  });

  testWidgets('loadRequest with headers', (WidgetTester tester) async {
    final headers = <String, String>{'test_header': 'flutter_test_header'};

    final pageLoads = StreamController<String>();

    final controller = PlatformWebViewController(
      const PlatformWebViewControllerCreationParams(),
    );
    unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
    final delegate = PlatformNavigationDelegate(
      const PlatformNavigationDelegateCreationParams(),
    );
    unawaited(delegate.setOnPageFinished((String url) => pageLoads.add(url)));
    unawaited(controller.setPlatformNavigationDelegate(delegate));
    await controller.loadRequest(
      LoadRequestParams(uri: Uri.parse(headersUrl), headers: headers),
    );

    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          return PlatformWebViewWidget(
            PlatformWebViewWidgetCreationParams(controller: controller),
          ).build(context);
        },
      ),
    );

    await pageLoads.stream.firstWhere((String url) => url == headersUrl);

    final content =
        await controller.runJavaScriptReturningResult(
              'document.documentElement.innerText',
            )
            as String;
    expect(content.contains('flutter_test_header'), isTrue);
  });

  testWidgets('loadFlutterAsset successfully loads an HTML asset', (
    WidgetTester tester,
  ) async {
    final pageFinished = Completer<void>();

    final controller = PlatformWebViewController(
      const PlatformWebViewControllerCreationParams(),
    );
    final delegate = PlatformNavigationDelegate(
      const PlatformNavigationDelegateCreationParams(),
    );
    unawaited(delegate.setOnPageFinished((_) => pageFinished.complete()));
    unawaited(controller.setPlatformNavigationDelegate(delegate));

    await expectLater(
      controller.loadFlutterAsset('assets/www/index.html'),
      completes,
    );

    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          return PlatformWebViewWidget(
            PlatformWebViewWidgetCreationParams(controller: controller),
          ).build(context);
        },
      ),
    );

    // Verify page also loads.
    await pageFinished.future;
  });

  testWidgets('JavascriptChannel', (WidgetTester tester) async {
    final pageFinished = Completer<void>();
    final controller = PlatformWebViewController(
      const PlatformWebViewControllerCreationParams(),
    );
    unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
    final delegate = PlatformNavigationDelegate(
      const PlatformNavigationDelegateCreationParams(),
    );
    unawaited(delegate.setOnPageFinished((_) => pageFinished.complete()));
    unawaited(controller.setPlatformNavigationDelegate(delegate));

    final channelCompleter = Completer<String>();
    await controller.addJavaScriptChannel(
      JavaScriptChannelParams(
        name: 'Echo',
        onMessageReceived: (JavaScriptMessage message) {
          channelCompleter.complete(message.message);
        },
      ),
    );

    await controller.loadHtmlString(
      'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+',
    );

    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          return PlatformWebViewWidget(
            PlatformWebViewWidgetCreationParams(controller: controller),
          ).build(context);
        },
      ),
    );

    await pageFinished.future;

    await controller.runJavaScript('Echo.postMessage("hello");');
    await expectLater(channelCompleter.future, completion('hello'));
  });

  testWidgets('JavaScriptChannel can receive undefined', (
    WidgetTester tester,
  ) async {
    final pageFinished = Completer<void>();
    final controller = PlatformWebViewController(
      const PlatformWebViewControllerCreationParams(),
    );
    unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
    final delegate = PlatformNavigationDelegate(
      const PlatformNavigationDelegateCreationParams(),
    );
    unawaited(delegate.setOnPageFinished((_) => pageFinished.complete()));
    unawaited(controller.setPlatformNavigationDelegate(delegate));

    final channelCompleter = Completer<String>();
    await controller.addJavaScriptChannel(
      JavaScriptChannelParams(
        name: 'Channel',
        onMessageReceived: (JavaScriptMessage message) {
          channelCompleter.complete(message.message);
        },
      ),
    );

    await controller.loadHtmlString(
      'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+',
    );

    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          return PlatformWebViewWidget(
            PlatformWebViewWidgetCreationParams(controller: controller),
          ).build(context);
        },
      ),
    );

    await pageFinished.future;

    await controller.runJavaScript('Channel.postMessage(undefined);');
    await expectLater(channelCompleter.future, completion('(null)'));
  });

  testWidgets('resize webview', (WidgetTester tester) async {
    final buttonTapResizeCompleter = Completer<void>();
    final onPageFinished = Completer<void>();

    var resizeButtonTapped = false;
    await tester.pumpWidget(
      ResizableWebView(
        onResize: () {
          if (resizeButtonTapped) {
            buttonTapResizeCompleter.complete();
          }
        },
        onPageFinished: () => onPageFinished.complete(),
      ),
    );

    await onPageFinished.future;

    resizeButtonTapped = true;

    await tester.tap(find.byKey(const ValueKey<String>('resizeButton')));
    await tester.pumpAndSettle();

    await expectLater(buttonTapResizeCompleter.future, completes);
  });

  testWidgets('set custom userAgent', (WidgetTester tester) async {
    final controller = PlatformWebViewController(
      const PlatformWebViewControllerCreationParams(),
    );
    unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
    unawaited(controller.setUserAgent('Custom_User_Agent1'));

    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          return PlatformWebViewWidget(
            PlatformWebViewWidgetCreationParams(controller: controller),
          ).build(context);
        },
      ),
    );

    final String? customUserAgent = await controller.getUserAgent();
    expect(customUserAgent, 'Custom_User_Agent1');
  });

  testWidgets(
    'getUserAgent returns a default value when custom value is not set',
    (WidgetTester tester) async {
      final controller = PlatformWebViewController(
        const PlatformWebViewControllerCreationParams(),
      );

      await tester.pumpWidget(
        Builder(
          builder: (BuildContext context) {
            return PlatformWebViewWidget(
              PlatformWebViewWidgetCreationParams(controller: controller),
            ).build(context);
          },
        ),
      );

      final String? userAgent = await controller.getUserAgent();
      expect(userAgent, isNotNull);
      expect(userAgent, isNotEmpty);
    },
  );

  group('Video playback policy', () {
    testWidgets('Auto media playback', (WidgetTester tester) async {
      final String videoTestBase64 = await getTestVideoBase64();
      var pageLoaded = Completer<void>();

      var controller = WebKitWebViewController(
        WebKitWebViewControllerCreationParams(
          mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
        ),
      );
      unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
      var delegate = WebKitNavigationDelegate(
        const PlatformNavigationDelegateCreationParams(),
      );
      unawaited(delegate.setOnPageFinished((_) => pageLoaded.complete()));
      unawaited(controller.setPlatformNavigationDelegate(delegate));

      await controller.loadRequest(
        LoadRequestParams(
          uri: Uri.parse(
            'data:text/html;charset=utf-8;base64,$videoTestBase64',
          ),
        ),
      );

      await tester.pumpWidget(
        Builder(
          builder: (BuildContext context) {
            return PlatformWebViewWidget(
              PlatformWebViewWidgetCreationParams(controller: controller),
            ).build(context);
          },
        ),
      );

      await pageLoaded.future;

      var isPaused =
          await controller.runJavaScriptReturningResult('isPaused();') as bool;
      expect(isPaused, false);

      pageLoaded = Completer<void>();
      controller = WebKitWebViewController(
        const PlatformWebViewControllerCreationParams(),
      );
      unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
      delegate = WebKitNavigationDelegate(
        const PlatformNavigationDelegateCreationParams(),
      );
      unawaited(delegate.setOnPageFinished((_) => pageLoaded.complete()));
      unawaited(controller.setPlatformNavigationDelegate(delegate));

      await controller.loadRequest(
        LoadRequestParams(
          uri: Uri.parse(
            'data:text/html;charset=utf-8;base64,$videoTestBase64',
          ),
        ),
      );

      await tester.pumpWidget(
        Builder(
          builder: (BuildContext context) {
            return PlatformWebViewWidget(
              PlatformWebViewWidgetCreationParams(controller: controller),
            ).build(context);
          },
        ),
      );

      await pageLoaded.future;

      isPaused =
          await controller.runJavaScriptReturningResult('isPaused();') as bool;
      expect(isPaused, true);
    });

    testWidgets('Video plays inline when allowsInlineMediaPlayback is true', (
      WidgetTester tester,
    ) async {
      final String videoTestBase64 = await getTestVideoBase64();
      final pageLoaded = Completer<void>();
      final videoPlaying = Completer<void>();

      final controller = PlatformWebViewController(
        WebKitWebViewControllerCreationParams(
          mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
          allowsInlineMediaPlayback: true,
        ),
      );
      unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
      final delegate = WebKitNavigationDelegate(
        const PlatformNavigationDelegateCreationParams(),
      );
      unawaited(delegate.setOnPageFinished((_) => pageLoaded.complete()));
      unawaited(controller.setPlatformNavigationDelegate(delegate));

      unawaited(
        controller.addJavaScriptChannel(
          JavaScriptChannelParams(
            name: 'VideoTestTime',
            onMessageReceived: (JavaScriptMessage message) {
              final double currentTime = double.parse(message.message);
              // Let it play for at least 1 second to make sure the related video's properties are set.
              if (currentTime > 1 && !videoPlaying.isCompleted) {
                videoPlaying.complete(null);
              }
            },
          ),
        ),
      );

      await controller.loadRequest(
        LoadRequestParams(
          uri: Uri.parse(
            'data:text/html;charset=utf-8;base64,$videoTestBase64',
          ),
        ),
      );

      await tester.pumpWidget(
        Builder(
          builder: (BuildContext context) {
            return PlatformWebViewWidget(
              PlatformWebViewWidgetCreationParams(controller: controller),
            ).build(context);
          },
        ),
      );
      await tester.pumpAndSettle();

      await pageLoaded.future;

      // Makes sure we get the correct event that indicates the video is actually playing.
      await videoPlaying.future;

      final fullScreen =
          await controller.runJavaScriptReturningResult('isFullScreen();')
              as bool;
      expect(fullScreen, false);
    });

    testWidgets(
      'Video plays full screen when allowsInlineMediaPlayback is false',
      (WidgetTester tester) async {
        final String videoTestBase64 = await getTestVideoBase64();
        final pageLoaded = Completer<void>();
        final videoPlaying = Completer<void>();

        final controller = PlatformWebViewController(
          WebKitWebViewControllerCreationParams(
            mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
          ),
        );
        unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
        final delegate = WebKitNavigationDelegate(
          const PlatformNavigationDelegateCreationParams(),
        );
        unawaited(delegate.setOnPageFinished((_) => pageLoaded.complete()));
        unawaited(controller.setPlatformNavigationDelegate(delegate));

        unawaited(
          controller.addJavaScriptChannel(
            JavaScriptChannelParams(
              name: 'VideoTestTime',
              onMessageReceived: (JavaScriptMessage message) {
                final double currentTime = double.parse(message.message);
                // Let it play for at least 1 second to make sure the related video's properties are set.
                if (currentTime > 1 && !videoPlaying.isCompleted) {
                  videoPlaying.complete(null);
                }
              },
            ),
          ),
        );

        await controller.loadRequest(
          LoadRequestParams(
            uri: Uri.parse(
              'data:text/html;charset=utf-8;base64,$videoTestBase64',
            ),
          ),
        );

        await tester.pumpWidget(
          Builder(
            builder: (BuildContext context) {
              return PlatformWebViewWidget(
                PlatformWebViewWidgetCreationParams(controller: controller),
              ).build(context);
            },
          ),
        );
        await tester.pumpAndSettle();

        await pageLoaded.future;

        // Makes sure we get the correct event that indicates the video is actually playing.
        await videoPlaying.future;

        final fullScreen =
            await controller.runJavaScriptReturningResult('isFullScreen();')
                as bool;
        expect(fullScreen, true);
      },
      // allowsInlineMediaPlayback has no effect on macOS.
      skip: Platform.isMacOS,
    );
  });

  group(
    'Audio playback policy',
    () {
      late String audioTestBase64;
      setUpAll(() async {
        final ByteData audioData = await rootBundle.load(
          'assets/sample_audio.ogg',
        );
        final String base64AudioData = base64Encode(
          Uint8List.view(audioData.buffer),
        );
        final audioTest =
            '''
        <!DOCTYPE html><html>
        <head><title>Audio auto play</title>
          <script type="text/javascript">
            function play() {
              var audio = document.getElementById("audio");
              audio.play();
            }
            function isPaused() {
              var audio = document.getElementById("audio");
              return audio.paused;
            }
          </script>
        </head>
        <body onload="play();">
        <audio controls id="audio">
          <source src="data:audio/ogg;charset=utf-8;base64,$base64AudioData">
        </audio>
        </body>
        </html>
      ''';
        audioTestBase64 = base64Encode(const Utf8Encoder().convert(audioTest));
      });

      testWidgets('Auto media playback', (WidgetTester tester) async {
        var pageLoaded = Completer<void>();

        var controller = PlatformWebViewController(
          WebKitWebViewControllerCreationParams(
            mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
          ),
        );
        unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
        var delegate = WebKitNavigationDelegate(
          const PlatformNavigationDelegateCreationParams(),
        );
        unawaited(delegate.setOnPageFinished((_) => pageLoaded.complete()));
        unawaited(controller.setPlatformNavigationDelegate(delegate));

        await controller.loadRequest(
          LoadRequestParams(
            uri: Uri.parse(
              'data:text/html;charset=utf-8;base64,$audioTestBase64',
            ),
          ),
        );

        await tester.pumpWidget(
          Builder(
            builder: (BuildContext context) {
              return PlatformWebViewWidget(
                PlatformWebViewWidgetCreationParams(controller: controller),
              ).build(context);
            },
          ),
        );

        await pageLoaded.future;

        var isPaused =
            await controller.runJavaScriptReturningResult('isPaused();')
                as bool;
        expect(isPaused, false);

        pageLoaded = Completer<void>();
        controller = PlatformWebViewController(
          WebKitWebViewControllerCreationParams(),
        );
        unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
        delegate = WebKitNavigationDelegate(
          const PlatformNavigationDelegateCreationParams(),
        );
        unawaited(delegate.setOnPageFinished((_) => pageLoaded.complete()));
        unawaited(controller.setPlatformNavigationDelegate(delegate));
        await controller.loadRequest(
          LoadRequestParams(
            uri: Uri.parse(
              'data:text/html;charset=utf-8;base64,$audioTestBase64',
            ),
          ),
        );

        await tester.pumpWidget(
          Builder(
            builder: (BuildContext context) {
              return PlatformWebViewWidget(
                PlatformWebViewWidgetCreationParams(controller: controller),
              ).build(context);
            },
          ),
        );

        await pageLoaded.future;

        isPaused =
            await controller.runJavaScriptReturningResult('isPaused();')
                as bool;
        expect(isPaused, true);
      });
    },
    // OGG playback is not supported on macOS, so the test data would need
    // to be changed to support macOS.
    skip: Platform.isMacOS,
  );

  testWidgets('getTitle', (WidgetTester tester) async {
    const getTitleTest = '''
        <!DOCTYPE html><html>
        <head><title>Some title</title>
        </head>
        <body>
        </body>
        </html>
      ''';
    final String getTitleTestBase64 = base64Encode(
      const Utf8Encoder().convert(getTitleTest),
    );
    final pageLoaded = Completer<void>();

    final controller = PlatformWebViewController(
      const PlatformWebViewControllerCreationParams(),
    );
    unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
    final delegate = PlatformNavigationDelegate(
      const PlatformNavigationDelegateCreationParams(),
    );
    unawaited(delegate.setOnPageFinished((_) => pageLoaded.complete()));
    unawaited(controller.setPlatformNavigationDelegate(delegate));

    await controller.loadRequest(
      LoadRequestParams(
        uri: Uri.parse(
          'data:text/html;charset=utf-8;base64,$getTitleTestBase64',
        ),
      ),
    );

    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          return PlatformWebViewWidget(
            PlatformWebViewWidgetCreationParams(controller: controller),
          ).build(context);
        },
      ),
    );

    await pageLoaded.future;

    // On at least iOS, it does not appear to be guaranteed that the native
    // code has the title when the page load completes. Execute some JavaScript
    // before checking the title to ensure that the page has been fully parsed
    // and processed.
    await controller.runJavaScript('1;');

    final String? title = await controller.getTitle();
    expect(title, 'Some title');
  });

  group(
    'Programmatic Scroll',
    () {
      testWidgets('setAndGetAndListenScrollPosition', (
        WidgetTester tester,
      ) async {
        const scrollTestPage = '''
        <!DOCTYPE html>
        <html>
          <head>
            <style>
              body {
                height: 100%;
                width: 100%;
              }
              #container{
                width:5000px;
                height:5000px;
            }
            </style>
          </head>
          <body>
            <div id="container"/>
          </body>
        </html>
      ''';

        final String scrollTestPageBase64 = base64Encode(
          const Utf8Encoder().convert(scrollTestPage),
        );

        final pageLoaded = Completer<void>();
        ScrollPositionChange? recordedPosition;
        final controller = PlatformWebViewController(
          const PlatformWebViewControllerCreationParams(),
        );
        unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
        final delegate = PlatformNavigationDelegate(
          const PlatformNavigationDelegateCreationParams(),
        );
        unawaited(delegate.setOnPageFinished((_) => pageLoaded.complete()));
        unawaited(controller.setPlatformNavigationDelegate(delegate));
        unawaited(
          controller.setOnScrollPositionChange((
            ScrollPositionChange scrollPositionChange,
          ) {
            recordedPosition = scrollPositionChange;
          }),
        );

        await controller.loadRequest(
          LoadRequestParams(
            uri: Uri.parse(
              'data:text/html;charset=utf-8;base64,$scrollTestPageBase64',
            ),
          ),
        );

        await tester.pumpWidget(
          Builder(
            builder: (BuildContext context) {
              return PlatformWebViewWidget(
                PlatformWebViewWidgetCreationParams(controller: controller),
              ).build(context);
            },
          ),
        );

        await pageLoaded.future;

        await tester.pumpAndSettle(const Duration(seconds: 3));

        Offset scrollPos = await controller.getScrollPosition();

        // Check scrollTo()
        const X_SCROLL = 123;
        const Y_SCROLL = 321;
        // Get the initial position; this ensures that scrollTo is actually
        // changing something, but also gives the native view's scroll position
        // time to settle.
        expect(scrollPos.dx, isNot(X_SCROLL));
        expect(scrollPos.dy, isNot(Y_SCROLL));
        expect(recordedPosition?.x, isNot(X_SCROLL));
        expect(recordedPosition?.y, isNot(Y_SCROLL));

        await controller.scrollTo(X_SCROLL, Y_SCROLL);
        scrollPos = await controller.getScrollPosition();
        expect(scrollPos.dx, X_SCROLL);
        expect(scrollPos.dy, Y_SCROLL);
        expect(recordedPosition?.x, X_SCROLL);
        expect(recordedPosition?.y, Y_SCROLL);

        // Check scrollBy() (on top of scrollTo())
        await controller.scrollBy(X_SCROLL, Y_SCROLL);
        scrollPos = await controller.getScrollPosition();
        expect(scrollPos.dx, X_SCROLL * 2);
        expect(scrollPos.dy, Y_SCROLL * 2);
        expect(recordedPosition?.x, X_SCROLL * 2);
        expect(recordedPosition?.y, Y_SCROLL * 2);
      });
    },
    // Scroll position is currently not implemented for macOS.
    // Flakes on iOS: https://github.com/flutter/flutter/issues/154826
    skip: Platform.isMacOS || Platform.isIOS,
  );

  group('NavigationDelegate', () {
    const blankPage = '<!DOCTYPE html><head></head><body></body></html>';
    final blankPageEncoded =
        'data:text/html;charset=utf-8;base64,'
        '${base64Encode(const Utf8Encoder().convert(blankPage))}';

    testWidgets('can allow requests', (WidgetTester tester) async {
      var pageLoaded = Completer<void>();

      final controller = PlatformWebViewController(
        const PlatformWebViewControllerCreationParams(),
      );
      unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
      final delegate = PlatformNavigationDelegate(
        const PlatformNavigationDelegateCreationParams(),
      );
      unawaited(delegate.setOnPageFinished((_) => pageLoaded.complete()));
      unawaited(
        delegate.setOnNavigationRequest((NavigationRequest navigationRequest) {
          return navigationRequest.url.contains('youtube.com')
              ? NavigationDecision.prevent
              : NavigationDecision.navigate;
        }),
      );
      unawaited(controller.setPlatformNavigationDelegate(delegate));

      await controller.loadRequest(
        LoadRequestParams(uri: Uri.parse(blankPageEncoded)),
      );

      await tester.pumpWidget(
        Builder(
          builder: (BuildContext context) {
            return PlatformWebViewWidget(
              PlatformWebViewWidgetCreationParams(controller: controller),
            ).build(context);
          },
        ),
      );

      await pageLoaded.future; // Wait for initial page load.

      pageLoaded = Completer<void>();
      await controller.runJavaScript('location.href = "$secondaryUrl"');
      await pageLoaded.future;

      final String? currentUrl = await controller.currentUrl();
      expect(currentUrl, secondaryUrl);
    });

    testWidgets('onWebResourceError', (WidgetTester tester) async {
      final errorCompleter = Completer<WebResourceError>();

      final controller = PlatformWebViewController(
        const PlatformWebViewControllerCreationParams(),
      );
      unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
      final delegate = PlatformNavigationDelegate(
        const PlatformNavigationDelegateCreationParams(),
      );
      unawaited(
        delegate.setOnWebResourceError((WebResourceError error) {
          errorCompleter.complete(error);
        }),
      );
      unawaited(controller.setPlatformNavigationDelegate(delegate));

      await controller.loadRequest(
        LoadRequestParams(uri: Uri.parse('https://www.notawebsite..com')),
      );

      await tester.pumpWidget(
        Builder(
          builder: (BuildContext context) {
            return PlatformWebViewWidget(
              PlatformWebViewWidgetCreationParams(controller: controller),
            ).build(context);
          },
        ),
      );

      final WebResourceError error = await errorCompleter.future;
      expect(error, isNotNull);
      expect(error.url?.startsWith('https://www.notawebsite..com'), isTrue);

      expect((error as WebKitWebResourceError).domain, isNotNull);
    });

    testWidgets('onWebResourceError is not called with valid url', (
      WidgetTester tester,
    ) async {
      final errorCompleter = Completer<WebResourceError>();
      final pageFinishCompleter = Completer<void>();

      final controller = PlatformWebViewController(
        const PlatformWebViewControllerCreationParams(),
      );
      unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
      final delegate = PlatformNavigationDelegate(
        const PlatformNavigationDelegateCreationParams(),
      );
      unawaited(
        delegate.setOnPageFinished((_) => pageFinishCompleter.complete()),
      );
      unawaited(
        delegate.setOnWebResourceError((WebResourceError error) {
          errorCompleter.complete(error);
        }),
      );
      unawaited(controller.setPlatformNavigationDelegate(delegate));
      await controller.loadRequest(
        LoadRequestParams(
          uri: Uri.parse(
            'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+',
          ),
        ),
      );

      await tester.pumpWidget(
        Builder(
          builder: (BuildContext context) {
            return PlatformWebViewWidget(
              PlatformWebViewWidgetCreationParams(controller: controller),
            ).build(context);
          },
        ),
      );

      expect(errorCompleter.future, doesNotComplete);
      await pageFinishCompleter.future;
    });

    testWidgets('onWebResourceError only called for main frame', (
      WidgetTester tester,
    ) async {
      const iframeTest = '''
          <!DOCTYPE html>
          <html>
          <head>
            <title>WebResourceError test</title>
          </head>
          <body>
            <iframe src="https://notawebsite..com"></iframe>
          </body>
          </html>
         ''';
      final String iframeTestBase64 = base64Encode(
        const Utf8Encoder().convert(iframeTest),
      );

      final errorCompleter = Completer<WebResourceError>();
      final pageFinishCompleter = Completer<void>();

      final controller = PlatformWebViewController(
        const PlatformWebViewControllerCreationParams(),
      );
      unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
      final delegate = PlatformNavigationDelegate(
        const PlatformNavigationDelegateCreationParams(),
      );
      unawaited(
        delegate.setOnPageFinished((_) => pageFinishCompleter.complete()),
      );
      unawaited(
        delegate.setOnWebResourceError((WebResourceError error) {
          errorCompleter.complete(error);
        }),
      );
      unawaited(controller.setPlatformNavigationDelegate(delegate));
      await controller.loadRequest(
        LoadRequestParams(
          uri: Uri.parse(
            'data:text/html;charset=utf-8;base64,$iframeTestBase64',
          ),
        ),
      );

      await tester.pumpWidget(
        Builder(
          builder: (BuildContext context) {
            return PlatformWebViewWidget(
              PlatformWebViewWidgetCreationParams(controller: controller),
            ).build(context);
          },
        ),
      );

      expect(errorCompleter.future, doesNotComplete);
      await pageFinishCompleter.future;
    });

    testWidgets('onHttpError', (WidgetTester tester) async {
      final errorCompleter = Completer<HttpResponseError>();

      final controller = PlatformWebViewController(
        const PlatformWebViewControllerCreationParams(),
      );
      unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
      final delegate = PlatformNavigationDelegate(
        const PlatformNavigationDelegateCreationParams(),
      );
      unawaited(
        delegate.setOnHttpError((HttpResponseError error) {
          errorCompleter.complete(error);
        }),
      );
      unawaited(controller.setPlatformNavigationDelegate(delegate));
      unawaited(
        controller.loadRequest(
          LoadRequestParams(uri: Uri.parse('$prefixUrl/favicon.ico')),
        ),
      );

      await tester.pumpWidget(
        Builder(
          builder: (BuildContext context) {
            return PlatformWebViewWidget(
              PlatformWebViewWidgetCreationParams(controller: controller),
            ).build(context);
          },
        ),
      );

      final HttpResponseError error = await errorCompleter.future;

      expect(error, isNotNull);
      expect(error.response?.statusCode, 404);
    });

    testWidgets('onHttpError is not called when no HTTP error is received', (
      WidgetTester tester,
    ) async {
      const testPage = '''
        <!DOCTYPE html><html>
        </head>
        <body>
        </body>
        </html>
      ''';

      final errorCompleter = Completer<HttpResponseError>();
      final pageFinishCompleter = Completer<void>();

      final controller = PlatformWebViewController(
        const PlatformWebViewControllerCreationParams(),
      );
      unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
      final delegate = PlatformNavigationDelegate(
        const PlatformNavigationDelegateCreationParams(),
      );
      unawaited(
        delegate.setOnHttpError((HttpResponseError error) {
          errorCompleter.complete(error);
        }),
      );
      unawaited(
        delegate.setOnPageFinished((_) => pageFinishCompleter.complete()),
      );
      unawaited(controller.setPlatformNavigationDelegate(delegate));
      unawaited(controller.loadHtmlString(testPage));

      await tester.pumpWidget(
        Builder(
          builder: (BuildContext context) {
            return PlatformWebViewWidget(
              PlatformWebViewWidgetCreationParams(controller: controller),
            ).build(context);
          },
        ),
      );

      expect(errorCompleter.future, doesNotComplete);
      await pageFinishCompleter.future;
    });

    testWidgets('can block requests', (WidgetTester tester) async {
      var pageLoaded = Completer<void>();

      final controller = PlatformWebViewController(
        const PlatformWebViewControllerCreationParams(),
      );
      unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
      final delegate = PlatformNavigationDelegate(
        const PlatformNavigationDelegateCreationParams(),
      );
      unawaited(delegate.setOnPageFinished((_) => pageLoaded.complete()));
      unawaited(
        delegate.setOnNavigationRequest((NavigationRequest navigationRequest) {
          return navigationRequest.url.contains('youtube.com')
              ? NavigationDecision.prevent
              : NavigationDecision.navigate;
        }),
      );
      unawaited(controller.setPlatformNavigationDelegate(delegate));

      await controller.loadRequest(
        LoadRequestParams(uri: Uri.parse(blankPageEncoded)),
      );

      await tester.pumpWidget(
        Builder(
          builder: (BuildContext context) {
            return PlatformWebViewWidget(
              PlatformWebViewWidgetCreationParams(controller: controller),
            ).build(context);
          },
        ),
      );

      await pageLoaded.future; // Wait for initial page load.

      pageLoaded = Completer<void>();
      await controller.runJavaScript(
        'location.href = "https://www.youtube.com/"',
      );

      // There should never be any second page load, since our new URL is
      // blocked. Still wait for a potential page change for some time in order
      // to give the test a chance to fail.
      await pageLoaded.future.timeout(
        const Duration(milliseconds: 500),
        onTimeout: () => '',
      );
      final String? currentUrl = await controller.currentUrl();
      expect(currentUrl, isNot(contains('youtube.com')));
    });

    testWidgets('supports asynchronous decisions', (WidgetTester tester) async {
      var pageLoaded = Completer<void>();

      final controller = PlatformWebViewController(
        const PlatformWebViewControllerCreationParams(),
      );
      unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
      final delegate = PlatformNavigationDelegate(
        const PlatformNavigationDelegateCreationParams(),
      );
      unawaited(delegate.setOnPageFinished((_) => pageLoaded.complete()));
      unawaited(
        delegate.setOnNavigationRequest((
          NavigationRequest navigationRequest,
        ) async {
          NavigationDecision decision = NavigationDecision.prevent;
          decision = await Future<NavigationDecision>.delayed(
            const Duration(milliseconds: 10),
            () => NavigationDecision.navigate,
          );
          return decision;
        }),
      );
      unawaited(controller.setPlatformNavigationDelegate(delegate));
      await controller.loadRequest(
        LoadRequestParams(uri: Uri.parse(blankPageEncoded)),
      );

      await tester.pumpWidget(
        Builder(
          builder: (BuildContext context) {
            return PlatformWebViewWidget(
              PlatformWebViewWidgetCreationParams(controller: controller),
            ).build(context);
          },
        ),
      );

      await pageLoaded.future; // Wait for initial page load.

      pageLoaded = Completer<void>();
      await controller.runJavaScript('location.href = "$secondaryUrl"');

      await pageLoaded.future; // Wait for second page to load.
      final String? currentUrl = await controller.currentUrl();
      expect(currentUrl, secondaryUrl);
    });

    testWidgets('can receive url changes', (WidgetTester tester) async {
      final pageLoaded = Completer<void>();

      final controller = PlatformWebViewController(
        const PlatformWebViewControllerCreationParams(),
      );
      unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
      final delegate = PlatformNavigationDelegate(
        const PlatformNavigationDelegateCreationParams(),
      );
      unawaited(delegate.setOnPageFinished((_) => pageLoaded.complete()));
      unawaited(controller.setPlatformNavigationDelegate(delegate));
      await controller.loadRequest(
        LoadRequestParams(uri: Uri.parse(blankPageEncoded)),
      );

      await tester.pumpWidget(
        Builder(
          builder: (BuildContext context) {
            return PlatformWebViewWidget(
              PlatformWebViewWidgetCreationParams(controller: controller),
            ).build(context);
          },
        ),
      );

      await pageLoaded.future;
      await delegate.setOnPageFinished((_) {});

      final urlChangeCompleter = Completer<String>();
      await delegate.setOnUrlChange((UrlChange change) {
        urlChangeCompleter.complete(change.url);
      });

      await controller.runJavaScript('location.href = "$primaryUrl"');

      await expectLater(urlChangeCompleter.future, completion(primaryUrl));
    });

    testWidgets('can receive updates to history state', (
      WidgetTester tester,
    ) async {
      final pageLoaded = Completer<void>();

      final controller = PlatformWebViewController(
        const PlatformWebViewControllerCreationParams(),
      );
      unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
      final delegate = PlatformNavigationDelegate(
        const PlatformNavigationDelegateCreationParams(),
      );
      unawaited(delegate.setOnPageFinished((_) => pageLoaded.complete()));
      unawaited(controller.setPlatformNavigationDelegate(delegate));
      await controller.loadRequest(
        LoadRequestParams(uri: Uri.parse(primaryUrl)),
      );

      await tester.pumpWidget(
        Builder(
          builder: (BuildContext context) {
            return PlatformWebViewWidget(
              PlatformWebViewWidgetCreationParams(controller: controller),
            ).build(context);
          },
        ),
      );

      await pageLoaded.future;
      await delegate.setOnPageFinished((_) {});

      final urlChangeCompleter = Completer<String>();
      await delegate.setOnUrlChange((UrlChange change) {
        urlChangeCompleter.complete(change.url);
      });

      await controller.runJavaScript(
        'window.history.pushState({}, "", "secondary.txt");',
      );

      await expectLater(urlChangeCompleter.future, completion(secondaryUrl));
    });
  });

  testWidgets('can receive HTTP basic auth requests', (
    WidgetTester tester,
  ) async {
    final authRequested = Completer<void>();
    final controller = PlatformWebViewController(
      const PlatformWebViewControllerCreationParams(),
    );

    final navigationDelegate = PlatformNavigationDelegate(
      const PlatformNavigationDelegateCreationParams(),
    );
    await navigationDelegate.setOnHttpAuthRequest(
      (HttpAuthRequest request) => authRequested.complete(),
    );
    await controller.setPlatformNavigationDelegate(navigationDelegate);

    // Clear cache so that the auth request is always received and we don't get
    // a cached response.
    await controller.clearCache();

    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          return PlatformWebViewWidget(
            WebKitWebViewWidgetCreationParams(controller: controller),
          ).build(context);
        },
      ),
    );

    await controller.loadRequest(
      LoadRequestParams(uri: Uri.parse(basicAuthUrl)),
    );

    await expectLater(authRequested.future, completes);
  });

  testWidgets('can reply to HTTP basic auth requests', (
    WidgetTester tester,
  ) async {
    final pageFinished = Completer<void>();
    final controller = PlatformWebViewController(
      const PlatformWebViewControllerCreationParams(),
    );

    final navigationDelegate = PlatformNavigationDelegate(
      const PlatformNavigationDelegateCreationParams(),
    );
    await navigationDelegate.setOnPageFinished((_) => pageFinished.complete());
    await navigationDelegate.setOnHttpAuthRequest(
      (HttpAuthRequest request) => request.onProceed(
        const WebViewCredential(user: 'user', password: 'password'),
      ),
    );
    await controller.setPlatformNavigationDelegate(navigationDelegate);

    // Clear cache so that the auth request is always received and we don't get
    // a cached response.
    await controller.clearCache();

    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          return PlatformWebViewWidget(
            WebKitWebViewWidgetCreationParams(controller: controller),
          ).build(context);
        },
      ),
    );

    await controller.loadRequest(
      LoadRequestParams(uri: Uri.parse(basicAuthUrl)),
    );

    await expectLater(pageFinished.future, completes);
  });

  testWidgets('launches with gestureNavigationEnabled on iOS', (
    WidgetTester tester,
  ) async {
    final controller = WebKitWebViewController(
      WebKitWebViewControllerCreationParams(),
    );
    unawaited(controller.setAllowsBackForwardNavigationGestures(true));
    await controller.loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl)));

    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          return PlatformWebViewWidget(
            PlatformWebViewWidgetCreationParams(controller: controller),
          ).build(context);
        },
      ),
    );

    final String? currentUrl = await controller.currentUrl();
    expect(currentUrl, primaryUrl);
  });

  testWidgets('target _blank opens in same window', (
    WidgetTester tester,
  ) async {
    final pageLoaded = Completer<void>();

    final controller = PlatformWebViewController(
      WebKitWebViewControllerCreationParams(),
    );
    unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
    final delegate = PlatformNavigationDelegate(
      const PlatformNavigationDelegateCreationParams(),
    );
    unawaited(delegate.setOnPageFinished((_) => pageLoaded.complete()));
    unawaited(controller.setPlatformNavigationDelegate(delegate));

    await controller.runJavaScript('window.open("$primaryUrl", "_blank")');

    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          return PlatformWebViewWidget(
            PlatformWebViewWidgetCreationParams(controller: controller),
          ).build(context);
        },
      ),
    );

    await pageLoaded.future;
    final String? currentUrl = await controller.currentUrl();
    expect(currentUrl, primaryUrl);
  });

  testWidgets('can open new window and go back', (WidgetTester tester) async {
    var pageLoaded = Completer<void>();

    final controller = PlatformWebViewController(
      const PlatformWebViewControllerCreationParams(),
    );
    unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
    final delegate = PlatformNavigationDelegate(
      const PlatformNavigationDelegateCreationParams(),
    );
    unawaited(delegate.setOnPageFinished((_) => pageLoaded.complete()));
    unawaited(controller.setPlatformNavigationDelegate(delegate));
    await controller.loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl)));

    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          return PlatformWebViewWidget(
            PlatformWebViewWidgetCreationParams(controller: controller),
          ).build(context);
        },
      ),
    );

    expect(controller.currentUrl(), completion(primaryUrl));
    await pageLoaded.future;
    pageLoaded = Completer<void>();

    await controller.runJavaScript('window.open("$secondaryUrl")');
    await pageLoaded.future;
    pageLoaded = Completer<void>();
    expect(controller.currentUrl(), completion(secondaryUrl));

    expect(controller.canGoBack(), completion(true));
    await controller.goBack();
    await pageLoaded.future;
    await expectLater(controller.currentUrl(), completion(primaryUrl));
  });

  testWidgets('can receive JavaScript alert dialogs', (
    WidgetTester tester,
  ) async {
    final controller = PlatformWebViewController(
      const PlatformWebViewControllerCreationParams(),
    );

    final alertMessage = Completer<String>();
    unawaited(
      controller.setOnJavaScriptAlertDialog((
        JavaScriptAlertDialogRequest request,
      ) async {
        alertMessage.complete(request.message);
      }),
    );

    unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
    unawaited(
      controller.loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))),
    );

    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          return PlatformWebViewWidget(
            PlatformWebViewWidgetCreationParams(controller: controller),
          ).build(context);
        },
      ),
    );

    await controller.runJavaScript('alert("alert message")');
    await expectLater(alertMessage.future, completion('alert message'));
  });

  testWidgets('can receive JavaScript confirm dialogs', (
    WidgetTester tester,
  ) async {
    final controller = PlatformWebViewController(
      const PlatformWebViewControllerCreationParams(),
    );

    final confirmMessage = Completer<String>();
    unawaited(
      controller.setOnJavaScriptConfirmDialog((
        JavaScriptConfirmDialogRequest request,
      ) async {
        confirmMessage.complete(request.message);
        return true;
      }),
    );

    unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
    unawaited(
      controller.loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))),
    );

    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          return PlatformWebViewWidget(
            PlatformWebViewWidgetCreationParams(controller: controller),
          ).build(context);
        },
      ),
    );

    await controller.runJavaScript('confirm("confirm message")');
    await expectLater(confirmMessage.future, completion('confirm message'));
  });

  testWidgets('can receive JavaScript prompt dialogs', (
    WidgetTester tester,
  ) async {
    final controller = PlatformWebViewController(
      const PlatformWebViewControllerCreationParams(),
    );

    unawaited(
      controller.setOnJavaScriptTextInputDialog((
        JavaScriptTextInputDialogRequest request,
      ) async {
        return 'return message';
      }),
    );

    unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));
    unawaited(
      controller.loadRequest(LoadRequestParams(uri: Uri.parse(primaryUrl))),
    );

    await tester.pumpWidget(
      Builder(
        builder: (BuildContext context) {
          return PlatformWebViewWidget(
            PlatformWebViewWidgetCreationParams(controller: controller),
          ).build(context);
        },
      ),
    );

    final Object promptResponse = await controller.runJavaScriptReturningResult(
      'prompt("input message", "default text")',
    );
    expect(promptResponse, 'return message');
  });

  group('Logging', () {
    testWidgets('can receive console log messages', (
      WidgetTester tester,
    ) async {
      const testPage = '''
          <!DOCTYPE html>
          <html>
          <head>
            <title>WebResourceError test</title>
          </head>
          <body onload="console.debug('Debug message')">
            <p>Test page</p>
          </body>
          </html>
         ''';

      final debugMessageReceived = Completer<String>();
      final controller = PlatformWebViewController(
        const PlatformWebViewControllerCreationParams(),
      );
      unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));

      await controller.setOnConsoleMessage((
        JavaScriptConsoleMessage consoleMessage,
      ) {
        debugMessageReceived.complete(
          '${consoleMessage.level.name}:${consoleMessage.message}',
        );
      });

      await controller.loadHtmlString(testPage);

      await tester.pumpWidget(
        Builder(
          builder: (BuildContext context) {
            return PlatformWebViewWidget(
              PlatformWebViewWidgetCreationParams(controller: controller),
            ).build(context);
          },
        ),
      );

      await expectLater(
        debugMessageReceived.future,
        completion('debug:Debug message'),
      );
    });

    testWidgets('can receive console log messages with cyclic object value', (
      WidgetTester tester,
    ) async {
      const testPage = '''
          <!DOCTYPE html>
          <html>
          <head>
            <title>WebResourceError test</title>
            <script type="text/javascript">
            function onLoad() {
              const obj1 = {
                name: "obj1",
              };
              const obj2 = {
                name: "obj2",
                obj1: obj1,
              };
              const obj = {
                obj1: obj1,
                obj2: obj2,
              };
              obj.self = obj;
              console.log(obj);
            }
          </script>
          </head>
          <body onload="onLoad();">
          </html>
         ''';

      final debugMessageReceived = Completer<String>();
      final controller = PlatformWebViewController(
        const PlatformWebViewControllerCreationParams(),
      );
      unawaited(controller.setJavaScriptMode(JavaScriptMode.unrestricted));

      await controller.setOnConsoleMessage((
        JavaScriptConsoleMessage consoleMessage,
      ) {
        debugMessageReceived.complete(
          '${consoleMessage.level.name}:${consoleMessage.message}',
        );
      });

      await controller.loadHtmlString(testPage);

      await tester.pumpWidget(
        Builder(
          builder: (BuildContext context) {
            return PlatformWebViewWidget(
              PlatformWebViewWidgetCreationParams(controller: controller),
            ).build(context);
          },
        ),
      );

      await expectLater(
        debugMessageReceived.future,
        completion(
          'log:{"obj1":{"name":"obj1"},"obj2":{"name":"obj2","obj1":{"name":"obj1"}}}',
        ),
      );
    });
  });
}

class ResizableWebView extends StatefulWidget {
  const ResizableWebView({
    super.key,
    required this.onResize,
    required this.onPageFinished,
  });

  final VoidCallback onResize;
  final VoidCallback onPageFinished;

  @override
  State<StatefulWidget> createState() => ResizableWebViewState();
}

class ResizableWebViewState extends State<ResizableWebView> {
  late final PlatformWebViewController controller =
      PlatformWebViewController(const PlatformWebViewControllerCreationParams())
        ..setJavaScriptMode(JavaScriptMode.unrestricted)
        ..setPlatformNavigationDelegate(
          WebKitNavigationDelegate(
            const WebKitNavigationDelegateCreationParams(),
          )..setOnPageFinished((_) => widget.onPageFinished()),
        )
        ..addJavaScriptChannel(
          JavaScriptChannelParams(
            name: 'Resize',
            onMessageReceived: (_) {
              widget.onResize();
            },
          ),
        )
        ..loadRequest(
          LoadRequestParams(
            uri: Uri.parse(
              'data:text/html;charset=utf-8;base64,${base64Encode(const Utf8Encoder().convert(resizePage))}',
            ),
          ),
        );

  double webViewWidth = 200;
  double webViewHeight = 200;

  static const String resizePage = '''
        <!DOCTYPE html><html>
        <head><title>Resize test</title>
          <script type="text/javascript">
            function onResize() {
              Resize.postMessage("resize");
            }
            function onLoad() {
              window.onresize = onResize;
            }
          </script>
        </head>
        <body onload="onLoad();" bgColor="blue">
        </body>
        </html>
      ''';

  @override
  Widget build(BuildContext context) {
    return Directionality(
      textDirection: TextDirection.ltr,
      child: Column(
        children: <Widget>[
          SizedBox(
            width: webViewWidth,
            height: webViewHeight,
            child: PlatformWebViewWidget(
              PlatformWebViewWidgetCreationParams(controller: controller),
            ).build(context),
          ),
          TextButton(
            key: const Key('resizeButton'),
            onPressed: () {
              setState(() {
                webViewWidth += 100.0;
                webViewHeight += 100.0;
              });
            },
            child: const Text('ResizeButton'),
          ),
        ],
      ),
    );
  }
}

class CopyableObjectWithCallback extends PigeonInternalProxyApiBaseClass {
  CopyableObjectWithCallback(this.callback);

  final VoidCallback callback;

  @override
  // ignore: non_constant_identifier_names
  CopyableObjectWithCallback pigeon_copy() {
    return CopyableObjectWithCallback(callback);
  }
}

class ClassWithCallbackClass {
  ClassWithCallbackClass() {
    callbackClass = CopyableObjectWithCallback(
      withWeakReferenceTo(this, (
        WeakReference<ClassWithCallbackClass> weakReference,
      ) {
        return () {
          // Weak reference to `this` in callback.
          // ignore: unnecessary_statements
          weakReference;
        };
      }),
    );
  }

  late final CopyableObjectWithCallback callbackClass;
}

Future<String> getTestVideoBase64() async {
  final ByteData videoData = await rootBundle.load('assets/sample_video.mp4');
  final String base64VideoData = base64Encode(Uint8List.view(videoData.buffer));
  final videoTest =
      '''
        <!DOCTYPE html><html>
        <head><title>Video auto play</title>
          <script type="text/javascript">
            function play() {
              var video = document.getElementById("video");
              video.play();
              video.addEventListener('timeupdate', videoTimeUpdateHandler, false);
            }
            function videoTimeUpdateHandler(e) {
              var video = document.getElementById("video");
              VideoTestTime.postMessage(video.currentTime);
            }
            function isPaused() {
              var video = document.getElementById("video");
              return video.paused;
            }
            function isFullScreen() {
              var video = document.getElementById("video");
              return video.webkitDisplayingFullscreen;
            }
          </script>
        </head>
        <body onload="play();">
        <video controls playsinline autoplay id="video">
          <source src="data:video/mp4;charset=utf-8;base64,$base64VideoData">
        </video>
        </body>
        </html>
      ''';
  return base64Encode(const Utf8Encoder().convert(videoTest));
}
